package microcredit.model.bo;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.persistence.TypedQuery;

import microcredit.model.Member;
import microcredit.model.MemberToLoan;
import microcredit.model.MemberToLoanEntry;
import microcredit.model.MemberToLoanHistory;
import microcredit.model.ValidationException;
import microcredit.model.domain.LoanExtensionReason;
import microcredit.model.domain.LoanStatus;
import microcredit.model.util.ModelUtils;

public class BOLoan extends BOBase {

	public List<MemberToLoan> loadByMemberID(Long memberID) {
		if (memberID == null) {
			return null;
		}
		
		open();
		try {
			TypedQuery<MemberToLoan> loanQuery = manager.createQuery("" +
					"SELECT loan " +
					"FROM MemberToLoan loan " +
					"WHERE " +
						"loan.memberID=:memberID ", MemberToLoan.class);
			
			loanQuery.setParameter("memberID", memberID);
			
			return loanQuery.getResultList();
		} finally {
			close();
		}
	}
	
	public MemberToLoan refresh(MemberToLoan loan) {
		if (loan.getMember() == null) {
			return loan;
		} else if (loan.getSequenceID() == null) {
			return loan;
		}
		
		open();
		try {
			TypedQuery<MemberToLoan> loanQuery = manager.createQuery("" +
					"SELECT loan " +
					"FROM MemberToLoan loan " +
					" WHERE " +
						"loan.memberID=:memberID " +
						"AND loan.sequenceID=:sequenceID", MemberToLoan.class);
			
			loanQuery.setParameter("memberID", loan.getMember().getId());
			loanQuery.setParameter("sequenceID", loan.getSequenceID());
			
			List<MemberToLoan> loans = loanQuery.getResultList();
			if (loans != null && loans.size() > 0) {
				loan = loans.get(0);
			}
			
			// lazy fetch data
			loan.getMemberToLoanEntries().size();
			loan.getMemberToLoanHistories().size();
			
			return loan;
		} finally {
			close();
		}
	}
	
	public MemberToLoan newLoan(Member m) {
		MemberToLoan loan = new MemberToLoan();
		loan.setMember(m);
		generateLoanEntries(loan);
		return loan;
	}
	
	public void delete(MemberToLoan loan) throws ValidationException {
		open();
		try {
			loan = manager.merge(loan);
			
			for (MemberToLoanEntry entry : loan.getMemberToLoanEntries()) {
				manager.remove(entry);
			}
			for (MemberToLoanHistory history : loan.getMemberToLoanHistories()) {
				manager.remove(history);
			}
			
			manager.remove(loan);
			
			commit();
		} finally {
			close();
		}
	}
	
	public MemberToLoan toggleAsCancelled(MemberToLoan loan) throws ValidationException  {
		
		// if still toggled as security repaid untoggle it
		if (loan.getLoanStatus() == LoanStatus.R) {
			toggleAsSecurityPaid(loan);
		}
		
		boolean toggle = loan.getLoanStatus() != LoanStatus.N;
		
		loan.setLoanStatus(toggle ? LoanStatus.N : LoanStatus.A);
		
		return loan;
	}
	
	public MemberToLoan toggleAsSecurityPaid(MemberToLoan loan) throws ValidationException {
		
		boolean toggle = loan.getLoanStatus() != LoanStatus.R; 
		
		loan.setLoanStatus(toggle ? LoanStatus.R : LoanStatus.A);
		
		int numberOfWeeks = loan.getSafeNumberOfWeeks();
		List<MemberToLoanEntry> loanEntries = loan.getMemberToLoanEntries();
		
		applyFixedToLoanEntries(loan);
		
		if (toggle) {
			for (int i = 0; i < numberOfWeeks; i++) {
				MemberToLoanEntry entry = null;
				if (i < loanEntries.size()) {
					entry = loanEntries.get(i);
				} else {
					continue;
				}

				if (entry.getFixed() == null
						|| !entry.getFixed().booleanValue()) {
					entry.setSecurityPaid(Boolean.valueOf(toggle));
				}
			}
		} else {
			// restore security paid for all entries not only for those with weeks
			if (loanEntries != null) {
				for (MemberToLoanEntry entry : loanEntries) {
					entry.setSecurityPaid(Boolean.valueOf(toggle));
				}
			}
		}
		
		
		return loan;
	}
	
	
	
	public MemberToLoan save(Member member, MemberToLoan loan) throws ValidationException {
		generateLoanEntries(loan);
		
		loan.validate();
		
		fillZerosForHoles(loan);
		open();
		try {
			
			applyFixedToLoanEntries(loan);
			generateMemberToLoanHistory(loan);
			
			loan = saveLoan(member, loan);
			
//			List<MemberToLoanHistory> histories = loan.getMemberToLoanHistories();
//			for (MemberToLoanHistory memberToLoanHistory : histories) {
//				manager.merge(memberToLoanHistory);
//			}
			
			commit();
		} finally {
			close();
		}
		return loan;
	}
	
	private void fillZerosForHoles(MemberToLoan loan) {
		List<MemberToLoanEntry> entries = loan.getMemberToLoanEntries();
		if (entries != null) {
			int highestIndex = -1;
			for (int i = 0; i < loan.getSafeNumberOfWeeks(); i++) {
				MemberToLoanEntry entry = entries.get(i);
				if (entry.getAmountPaid() != null) {
					highestIndex = i;
				}
			}
			for (int i = 0; i < loan.getSafeNumberOfWeeks(); i++) {
				MemberToLoanEntry entry = entries.get(i);
				if (entry.getAmountPaid() == null 
						&& i < highestIndex) {
					entry.setAmountPaid(new BigDecimal(0));
				}
			}
		}
	}
	
	
	private void updateStatus(MemberToLoan loan) {
		
		if (loan.getLoanStatus() == LoanStatus.A) {
			// if pending
			if (ModelUtils.compare(loan.getReturnAmount(), loan.getAmountPaid()) <= 0) {
				loan.setLoanStatus(LoanStatus.F);
			}
		} else if (loan.getLoanStatus() == LoanStatus.F) {
			if (ModelUtils.compare(loan.getReturnAmount(), loan.getAmountPaid()) > 0) {
				loan.setLoanStatus(LoanStatus.A);
			}	
		} else if (loan.getLoanStatus() == LoanStatus.R) {
			if (ModelUtils.compare(loan.getReturnAmount(), loan.getAmountPaid()) <= 0) {
				loan.setLoanStatus(LoanStatus.K);
			}
		} else if (loan.getLoanStatus() == LoanStatus.K) {
			if (ModelUtils.compare(loan.getReturnAmount(), loan.getAmountPaid()) > 0) {
				loan.setLoanStatus(LoanStatus.R);
			}
		}
		
	}

	private void generateMemberToLoanHistory(MemberToLoan loan) {
		if (loan.isNew() && !loan.hasNewExtension()) {
			// if new generate a new member to loan history
			MemberToLoanHistory history = new MemberToLoanHistory();
			history.setExtensionReason(LoanExtensionReason.D);
			history.setMemberToLoan(loan);
			history.setNewNumberOfWeeks(loan.getNumberOfWeeks());
			history.setStartDate(loan.getStartDate());
			if (loan.getMemberToLoanHistories() == null) {
				loan.setMemberToLoanHistories(new ArrayList<MemberToLoanHistory>());
			}
			loan.getMemberToLoanHistories().add(history);
		}
	}
	
	
	private void validateCustomSequenceID(Member m, Long id) throws ValidationException {
		TypedQuery<Long> query = manager.createQuery("SELECT count(l) FROM MemberToLoan l WHERE " +
				" l.memberID=:memberID AND l.sequenceID=:sequenceID", Long.class);
		query.setParameter("memberID", m.getId());
		query.setParameter("sequenceID", id);
		Long result = query.getSingleResult();
		if (result > 0) {
			throw new ValidationException("The defined custom loan number '"+(id+1)+"' is already taken.", MemberToLoan.SEQUENCE_ID);
		}
	}
	
	private MemberToLoan saveLoan(Member member, MemberToLoan loan) throws ValidationException {
		boolean isnew = loan.isNew();
		
		// apply sequence id to member to loan
		if (loan.getSequenceID() == null) {
			Long sequenceID = generateSequenceID(member);
			if (loan.getCustomSequenceID() != null && sequenceID != loan.getCustomSequenceID()) {
				validateCustomSequenceID(member, loan.getCustomSequenceID());
				loan.setSequenceID(loan.getCustomSequenceID());
			} else {
				loan.setSequenceID(sequenceID);
			}
		}
		
		loan.setMember(member);
		
		// first remove unnecessary week entries
		
		if (isnew) {
			loan = manager.merge(loan);
		}
		
		// save entries -> remove unnecessary entries
		List<MemberToLoanEntry> entries = loan.getMemberToLoanEntries();
		List<MemberToLoanEntry> toRemove = new ArrayList<MemberToLoanEntry>();
		int index = 0;
		for (MemberToLoanEntry memberToLoanEntry : entries) {
			memberToLoanEntry.setMemberToLoan(loan);
			if (index < loan.getSafeNumberOfWeeks()) {
				manager.merge(memberToLoanEntry);
			} else {
				toRemove.add(memberToLoanEntry);
			}
			index++;
		}
		entries.removeAll(toRemove);
		for (MemberToLoanEntry memberToLoanEntry : toRemove) {
			memberToLoanEntry = manager.merge(memberToLoanEntry);
			manager.remove(memberToLoanEntry);
		}
		

		List<MemberToLoanHistory> removed = new ArrayList<MemberToLoanHistory>();
		List<MemberToLoanHistory> actual = loan.getMemberToLoanHistories();
		List<MemberToLoanHistory> valid = loan.getValidMemberToLoanHistories();
		for (MemberToLoanHistory history : actual) {
			if (!valid.contains(history)) {
				removed.add(history);
				history = manager.merge(history);
				manager.remove(history);
			}
		}
		actual.removeAll(removed);
		index = 0;
		for (MemberToLoanHistory history : actual) {
			history.setMemberToLoan(loan);
			history.setHistorySequenceID(index);
			
			manager.merge(history);
			index++;
		}
		
		if (!isnew) {
			loan = manager.merge(loan);
		}
		return loan;
	}

	public Long generateSequenceID(Member member) {
		Long maxLoanID = null;
		for (MemberToLoan subLoan : member.getMemberToLoans()) {
			if (subLoan.getSequenceID() != null && 
					(maxLoanID == null || subLoan.getSequenceID() >= maxLoanID)) {
				maxLoanID = subLoan.getSequenceID();
			}
		}
		if (maxLoanID == null) {
			maxLoanID = -1L;
		}
		maxLoanID++;
		return maxLoanID;
	}
	
	private void applyFixedToLoanEntries(MemberToLoan loan) {
		int numberOfWeeks = loan.getSafeNumberOfWeeks();
		List<MemberToLoanEntry> loanEntries = loan.getMemberToLoanEntries();
		for (int i = 0; i < numberOfWeeks; i++) {
			MemberToLoanEntry entry = null;
			if (i < loanEntries.size()) {
				entry = loanEntries.get(i);
			} else {
				continue;
			}
			if (entry.getAmountPaid() != null || 
					loan.getLoanStatus() == LoanStatus.F) {
				entry.setFixed(Boolean.TRUE);
			}
		}
	}
	
	public void generateExtension(MemberToLoan loan) throws ValidationException {
		if (!loan.hasNewExtension()) {
			if (!loan.isEntryFinalized() || loan.isFinalized()) {
				throw new ValidationException("Cannot generate extension, no paid amount and/or loan is finalized.");
			}
			fillZerosForHoles(loan);
			
			if (loan.getMemberToLoanHistories() == null) {
				loan.setMemberToLoanHistories(new ArrayList<MemberToLoanHistory>());
			}
			
			Integer historyIndex = loan.getHistoryIndex();
			MemberToLoanHistory history;
			
			if (historyIndex == null || historyIndex >= loan.getMemberToLoanHistories().size()) {
				history = new MemberToLoanHistory();
				history.setMemberToLoan(loan);
				loan.getMemberToLoanHistories().add(history);
				
				// reset history index since it is not needed anymore
				// valid index >= entries size
				loan.setHistoryIndex(null);
			} else {
				// if history can be taken from existing loan entries reuse it
				history = loan.getMemberToLoanHistories().get(historyIndex);
				historyIndex++;
				loan.setHistoryIndex(historyIndex);
			}
			
			for (int i = 0; i < loan.getSafeNumberOfWeeks(); i++) {
				MemberToLoanEntry entry = loan.getMemberToLoanEntries().get(i);
				if (entry.getAmountPaid() != null) {
					entry.setLocked(Boolean.TRUE);
					entry.setFixed(Boolean.TRUE);
				}
			}
			history.setReasonText(null);
			history.setExtensionReason(null);
			history.setLoanAmount(loan.getLoanAmount());
			history.setInterestPercentage(loan.getInterestPercentage());
			history.setStartDate(loan.calculateLockedStartDate());
			history.setRepaymentRate(loan.getRepaymentRate());
			generateLoanEntries(loan);
		}
	}
	
	public void removeLastExtension(MemberToLoan loan) {
		MemberToLoanHistory history = loan.getLastHistory();
		MemberToLoanHistory lastHistory = loan.getLastLastHistory();
		if (lastHistory == null) {
			throw new ValidationException("Could not restore last history. Database inconsistency?");
		}
		loan.setHistoryIndex(loan.getMemberToLoanHistories().indexOf(history));
		
		// now unfix the loan entries
		Date startDate = lastHistory.getStartDate();
		for (int i = 0; i < loan.getSafeNumberOfWeeks(); i++) {
			MemberToLoanEntry entry = loan.getMemberToLoanEntries().get(i);
			if (entry.getStartDate().compareTo(startDate) >= 0) {
				entry.setLocked(Boolean.FALSE);
			}
		}
		updateByHistory(loan, loan.getLastHistory());
		generateLoanEntries(loan);
	}

	private void updateByHistory(MemberToLoan loan, MemberToLoanHistory history) {
		
		BigDecimal interestPercentage = history.getInterestPercentage();
		BigDecimal loanAmount = history.getLoanAmount();
		BigDecimal repaymentRate = history.getRepaymentRate();
		Integer numberOfWeeks = history.getNewNumberOfWeeks();
		
		// again for compatibility take over loan amount and interest into history if
		// set in membertoloan and not in history.
		if (loan.getLoanAmount() != null && history.getLoanAmount() == null) {
			history.setLoanAmount(loan.getLoanAmount());
		}
		if (loan.getInterestPercentage() != null && history.getInterestPercentage() == null) {
			history.setInterestPercentage(loan.getInterestPercentage());
		}

		
		// important for backwards compatibility -> loan amount cannot be null
//		if (loanAmount != null) {
			loan.setLoanAmount(loanAmount);
//		}
//		if (interestPercentage != null){
			loan.setInterestPercentage(interestPercentage);
//		}
		if (numberOfWeeks != null) {
			loan.setNumberOfWeeks(numberOfWeeks != null ? numberOfWeeks : 0);
		}
//		if (repaymentRate != null) {
			loan.setRepaymentRate(repaymentRate);
//		}
		
		if (history.getExtensionReason() == LoanExtensionReason.D) {
			history.setStartDate(loan.getStartDate());
		}

		
	}
	
	public void generateLoanEntries(MemberToLoan loan) throws ValidationException { 
		
		generateMemberToLoanHistory(loan);
		
		if (loan.getLastHistory() != null) {
			updateByHistory(loan, loan.getLastHistory());
		}
		
		int calculatedNumberOfWeeks = calculateNumberOfWeeks(loan);
		
		loan.setNumberOfWeeks(calculatedNumberOfWeeks);
		if (loan.getLastHistory() != null) {
			loan.getLastHistory().setNewNumberOfWeeks(calculatedNumberOfWeeks);
		}
		
		int numberOfWeeks = loan.getSafeNumberOfWeeks();
		
		if (numberOfWeeks == 0) {
			return;
		}
		
		int entryLockedIndex = loan.findEntryLockedIndex();
		int unlockedNumberOfWeeks = numberOfWeeks - entryLockedIndex;
		if (unlockedNumberOfWeeks == 0) {
			return;
		}
		
		BigDecimal returnAmount = loan.getReturnAmount();
		if (returnAmount == null) {
			returnAmount = new BigDecimal(0);
		}
		
		BigDecimal lockedAmount = calculateLockedAmount(loan);
		returnAmount = returnAmount.subtract(lockedAmount);
		
		BigDecimal weekAmount;
		if (loan.getRepaymentRate() != null) {
			weekAmount = loan.getRepaymentRate();
		} else {
			weekAmount = calculateRepaymentRate(unlockedNumberOfWeeks, returnAmount);
		}
		
		BigDecimal roundingDifference = returnAmount.subtract(weekAmount.multiply(new BigDecimal(unlockedNumberOfWeeks)));
		roundingDifference = roundingDifference.setScale(2, BigDecimal.ROUND_HALF_UP);
		
		List<MemberToLoanEntry> loanEntries = loan.getMemberToLoanEntries();
		if (loanEntries == null) {
			loanEntries = new ArrayList<MemberToLoanEntry>();
		}
		
//		while (loanEntries.size() > numberOfWeeks) {
//			loanEntries.remove(loanEntries.size()-1);
//		}
		
		BigDecimal amountPaid = new BigDecimal(0);
		
		Date startDate = findCurrentStartDate(loan);
		
		for (int i = 0; i < numberOfWeeks; i++) {
			MemberToLoanEntry entry = null;
			if (i < loanEntries.size()) {
				entry = loanEntries.get(i);
			}
			
			if (entry == null) {
				entry = generateLoanEntry(loan, i, weekAmount);
				loanEntries.add(entry);
			}
			
			if (entry.getAmountPaid() != null) {
				amountPaid =amountPaid.add(entry.getAmountPaid());
			}
			
			// only add up entries which are belock entry locked index
			if (i < entryLockedIndex) {
				continue;
			}
			
			entry.setMemberToLoan(loan);
			
			if (i == numberOfWeeks-1) {
				entry.setAmount(weekAmount.add(roundingDifference));
			} else {
				entry.setAmount(weekAmount);
			}
			
			calculateDates(startDate, entry, i, entryLockedIndex);
		}
		
		if (loanEntries.size() > 0 && loan.isPaid()) {
			Date finalizedDate = null;
			for (int i = numberOfWeeks-1; i >= 0; i--) {
				if (loanEntries.size() <= i) {
					continue;
				}
				MemberToLoanEntry entry = loanEntries.get(i);
				if (entry.getAmountPaid() != null
						&& entry.getAmountPaid().compareTo(new BigDecimal(0)) > 0) {
					finalizedDate = entry.getEndDate();
					break;
				}
			}
			// update end date
			loan.setFinalizedDate(finalizedDate);
		} else {
			loan.setFinalizedDate(null);
		}
		
		loan.setAmountPaid(amountPaid);
		loan.setMemberToLoanEntries(loanEntries);
		
		
		updateStatus(loan);
	}

	private BigDecimal calculateRepaymentRate(int numberOfWeeks,
			BigDecimal returnAmount) {
		BigDecimal weekAmount = returnAmount.divide(new BigDecimal(numberOfWeeks), 2, BigDecimal.ROUND_HALF_UP);
		return weekAmount;
	}
	
	private int calculateNumberOfWeeks(MemberToLoan loan) {
		BigDecimal returnAmount = loan.getReturnAmount();
		if (loan.getRepaymentRate() == null || returnAmount == null) {
			return loan.getNumberOfWeeks();
		}
		
		BigDecimal repaymentRate = loan.getRepaymentRate();
		if (repaymentRate == null) {
			repaymentRate = new BigDecimal(0);
		}
		if (repaymentRate.compareTo(new BigDecimal(0)) <= 0) {
			loan.setRepaymentRate(null);
			return loan.getNumberOfWeeks();
		}
		returnAmount = returnAmount.subtract(calculateLockedAmount(loan));
		
		BigDecimal numberOfWeeks = returnAmount.divide(repaymentRate, 0, BigDecimal.ROUND_UP);
		int newNumberOfWeeks = numberOfWeeks.intValue();
		newNumberOfWeeks += loan.findEntryLockedIndex();
		if (newNumberOfWeeks < 0) {
			newNumberOfWeeks = 0;
		}
		return newNumberOfWeeks;
	}
	
	private void calculateDates(Date startDate, MemberToLoanEntry entry, int index, int entryLockedIndex) {
		if (startDate == null) {
			entry.setStartDate(null);
			entry.setEndDate(null);
		} else {
			Calendar c = Calendar.getInstance();
			c.setTime(startDate);
			
			long weekIndex = index - entryLockedIndex;
			c.add(Calendar.WEEK_OF_YEAR, (int)weekIndex);
			
			entry.setStartDate(c.getTime());
			c.add(Calendar.DAY_OF_YEAR, 6);
			entry.setEndDate(c.getTime());
		}
	}
	
	private BigDecimal calculateLockedAmount(MemberToLoan loan) {
		BigDecimal lockedAmount = new BigDecimal(0);
		if (loan.getMemberToLoanEntries() != null) {
			for (int i = 0; i < loan.getSafeNumberOfWeeks(); i++) {
				List<MemberToLoanEntry> entries = loan.getMemberToLoanEntries();
				if (i < entries.size()) {
					MemberToLoanEntry entry = entries.get(i);
					if (entry.getLocked() != null && entry.getLocked().booleanValue()) {
						lockedAmount = lockedAmount.add(entry.getAmountPaid());
					} else {
						break;
					}
				} else {
					// member to loan still null we assume that nothing is paid
					break;
				}
			}
		}
		return lockedAmount;
	}
	
	private Date findCurrentStartDate(MemberToLoan loan) {
		if (loan.getLastHistory() == null
				|| loan.getLastHistory().getExtensionReason() == LoanExtensionReason.D) {
			if (loan.getRepaymentStartDate() != null) {
				return loan.getRepaymentStartDate();
			}
			return loan.getStartDate();
		}
		return loan.getLastHistory().getStartDate();
	}
	
	private MemberToLoanEntry generateLoanEntry(MemberToLoan loan, int sequenceID, BigDecimal weekAmount) {
		MemberToLoanEntry entry = new MemberToLoanEntry();
		entry.setMemberToLoan(loan);
		entry.setEntrySequenceID(Long.valueOf(sequenceID));
		entry.setAmount(weekAmount);
		entry.setSecurityPaid(Boolean.valueOf(loan.getLoanStatus() == LoanStatus.K || loan.getLoanStatus() == LoanStatus.R));
		entry.setAmountPaid(null);
		return entry;
	}

	
	public static BOLoan create() {
		return new BOLoan();
	}
	
	
	public static void main(String[] args) {
		
	}
	
}
